""" app.py - Application/game management using the event system.

    TODO: Document me!
    
    ~~~ Module Attributes ~~~
    
    default:    The first app created. Before any app is created, this
                will be None.
    
    
    FIXME:  GUI applications using the App class may overlaod the system event queue with CE_DISPLAY/CE_REFRESH
"""

import weakref
import operator
import pygame
from pygame import Rect, QUIT, KEYDOWN, K_ESCAPE

from sbak.video import (
    update as video_update,
    get_screen
)
from sbak import(
    error,
    event,
    resman,
    world
)
from sbak.error import AppExit
from sbak.event import (
    CONTROLEVENT,
    CE_CYCLE,
    CE_UPDATE,
    CE_DISPLAY,
    CE_REFRESH,
    CE_COLLISION,
    get as event_get,
    Event,
    EventQueueManager
)
#from sbak import entity

__all__ = (
    'App',
    'GUIApp',
    'Game',
)

# The default engine speed
DEFAULT_TICKS = 30

# The default priority for hooks created by apps.
DEFAULT_HOOK_PRI = 1000000

# See the attribute list at the top of this module.
default = None

class App(EventQueueManager):
    """ A general-purpose app.

        myapp = App(resources = None)
        
        ~~~ Constructor Arguments ~~~

        resources:  A resource manager for children of the app to use
                    to load resources. If None (default), a
                    `ResourceManager` object is created for it.

        ~~~ Description ~~~

        TO DO: More documentation!

        This class defines functionality that is common to all app
        types.
        
        When an app is initialized it will hook a few handlers. These
        are listed below. Remember not to hook these handlers in
        subclasses or else they will be hooked twice, and will end up
        getting triggered twice everytime the corresponding event
        occurs.
        
        ~~~ Default Handlers Hooked ~~~
        
        Handler             Hook Type and State
        
        self.handle_cycle   {type : CONTROLEVENT, control : CE_CYCLE}
        self.handle_update  {type : CONTROLEVENT, control : CE_UPDATE}
        self.handle_display {type : CONTROLEVENT, control : CE_DISPLAY}
        self.handle_refresh {type : CONTROLEVENT, control : CE_REFRESH}
        self.finish         {type : QUIT}
        
        ~~~ Attributes ~~~
        
        resources:  A resource manager for children of the app to use
                    to load resources.
        
        ~~~ Notes ~~~
        
        TODO: Document how to properly implement an event handler loop.
    """
    __slots__ = ['_quit', 'resources']
    
    def __init__(self,  resources = None ):
        global default
        
        super(App,self).__init__()
        
        # Used to start/stop/finish the main event loop.
        self._quit = True
        
        # Find out whether or not to create a resource manager.
        if resources is None:
            self.resources = resman.ResourceManager()
            self.resources.set_basics()
        else:
            self.resources = resources
        
        # Hook the CONTROLEVENT handlers.
        cehandlers = {
            CE_CYCLE : self.handle_cycle,
            CE_UPDATE : self.handle_update,
            CE_DISPLAY : self.handle_display,
            CE_REFRESH : self.handle_refresh
        }
        for t, h in cehandlers.iteritems():
            self.hook(h, CONTROLEVENT, priority=DEFAULT_HOOK_PRI, control=t)
        
        # Hook the QUIT handler.
        self.hook(
            callback =  self.finish,
            type =      QUIT,
            priority =  DEFAULT_HOOK_PRI
        )
        
        # If no default has yet been set, use self as the default.
        if default is None:
            default = self
    
    def post_system_events(self):
        """ Posts all system events to the App's event queue.
        
            As the name suggests, this method gets each event that is
            currently on the Pygame system event queue and posts it
            into the App's own event queue, effectively moving the
            content of the system queue to the app's queue.
            
            This method is commonly used in the CE_CYCLE handler of
            apps as a means of making sure that system events only
            appear between the CE_CYCLE and--if present--CE_UPDATE
            event(s).
        """
        post = self.post
        for e in event_get():
            post(e)
    
    def handle_cycle(self, e):
        """ Posts a CE_CYCLE event, unless App.finish has been called.
        
            Also, this calls pygame.event.pump, which will attempt to
            get any system events that may have occurred to appear on
            the system event queue.
        
            Posting the CE_CYCLE event will cause the main loop to be
            perpetuated after it has started. Calling `App.finish`
            sets an internal flag which causes this handler method
            to raise an AppExit exception, thereby breaking out of
            the main loop.
        """
        if self._quit:
            raise error.AppExit()
        self.post_system_events()
        self.post(Event(CONTROLEVENT, control = CE_UPDATE))
    
    def handle_update(self, e):
        """ CE_UPDATE handler for App.
        
            This will attempt to pop the oldest dirty region on the
            screen and post a CE_DISPLAY event for it. Failing that,
            (ie. when their are no dirty regions) the next option is to
            post a CE_CYCLE event, thus ending the current cycle and
            beginning the next one.
        """
        self.post(Event(
            CONTROLEVENT,
            control = CE_DISPLAY,
            surface = get_screen(),
            offset = (0,0),
            region = None
        ))
    
    def handle_display(self, e):
        """ CE_DISPLAY handler for App.
        
            This will attempt to pop the oldest dirty region on the
            screen and post another CE_DISPLAY event for it. Failing
            that, (ie. when their are no dirty regions) the next option
            is to post a CE_REFRESH event. This effectively creates a
            "display loop" in which display events continue to be
            posted until there are no more dirty regions.
        """
        
        self.post(Event(
            CONTROLEVENT,
            control = CE_REFRESH,
            regions = None
        ))
    
    def handle_refresh(self, e):
        """ CE_REFRESH handler for App.
        
            This will update the screen with the regions indicated by
            the event, then post a CE_CYCLE event to continue the
            control cycle.
        """
        regions = e.regions
        if regions is not None:
            video_update(regions)
        else:
            video_update()
        self.post(Event(CONTROLEVENT, control = CE_CYCLE))
    
    def start(self, startpoint = CE_CYCLE):
        """ Starts (or re-starts) the main event loop.
        
            If the app is already started, this does nothing.
            
            An optional "start point" may be specified. This is a
            sbak.event.CE_* constant (a control constant). This
            control event will be posted before starting the event
            loop. The default is CE_CYCLE, which represents the
            beginning of a cycle of the main event loop. Pass None
            to not give a startpoint, but be aware that the event
            loop will simply end as soon as it started if there are
            no events in the queue.
        """
        if self._quit:
            self._quit = False
            if startpoint is not None:
                self.post(Event(CONTROLEVENT, control = startpoint))
            try:
                self.dispatch_repeat()
            except AppExit:
                pass
    
    def finish(self, e=None):
        """ Causes the main loop to end after the current cycle ends.
        
            NOTE: In order for this method to work, one of the event
            handlers used in the app's main event loop must raise
            `sbak.error.AppExit` when it detects that the app's
            internal attribute `_quit` is nonzero.
            
            The event handlers of both GUIApp and Game implement this
            behavior correctly. (FIXME: Unverified.)
            
            This method can be used as an event handler.
        """
        self._quit = True
    
    def stop(self):
        """ Stops the app's main event loop immediately.
            
            The loop will stop at the position in the event queue that
            is currently being handled. Hooks that have not been called
            yet will not be called, and unhandled events will remain in
            the queue for when the app is started again.
            
            If the app is already stopped, nothing happens.
            
            TIP: If you call this method from inside a control event
            handler that is hooked with a higher priority than the
            app's default handler for that event, the main event loop
            can be started right where it left off simply by calling
            app_obj.start(None).
            
            FIXME: This method will fail outside the main loop, therefore it is not thread safe.
        """
        if not self._quit:
            self._quit = True
            raise AppExit()

class Game(App):
    __doc__ =\
    """ Game application manager.
    
        mygame = Game(ticks = %(DEFAULT_TICKS)s, resources = None, universe = None)
    
        ~~~ Constructor Arguments ~~~
        
        ticks:      Ticks per second. The Game will try to have no more
                    main loop cycles per second than this value (though
                    there may be less). The default is %(DEFAULT_TICKS)s.
        
        resources:  A resource manager for worlds, entities, and other
                    objects to use to load resources dynamically.
                    If None (default), a `sbak.resman.Resource` object
                    will be created for it.
        
        universe:   A universe to hold worlds. All worlds in the game
                    should go in here when loaded. If None is passed,
                    a `sbak.world.Universe` object is created for it.
                    This universe can later be accessed as
                    `mygame.universe`.
        
        ~~~ Description ~~~
        
        TO DO: More documentation!
        
        Games hook their .finish method to the KEYDOWN/K_ESCAPE
        event and the QUIT event. This allows the game to be quit at
        any time by pressing the Escape key or the window close button.
    
        ~~~ Attributes ~~~
        
        ticks:  The number of ticks per second. This will determine
                how many times the game is updated every second.
                This effectively determines the target FPS as well.
        
        clock:  The clock used to regulate game speed.
        
        resources:  The resource manager used by objects of the game.
    """ % {'DEFAULT_TICKS':DEFAULT_TICKS}
    
    def __init__( self, ticks = DEFAULT_TICKS,
                        resources = None,
                        universe = None):
        App.__init__(self, resources = resources)
        
        # Time regulation
        
        self.clock = pygame.time.Clock()
        self.ticks = ticks
        
        # Append the universe, creating it first if necessary
        if universe is None:
            universe = world.Universe()
        self.append(universe)
        
        # Hook the quit key handler
        self.hook(self.finish, KEYDOWN, PRI, key = K_ESCAPE)

    # Event handlers
    
    def handle_cycle(self, e):
        """ Game's CE_CYCLE event handler.
        
            Regulates game speed using a Pygame Clock, posts system
            events into the Game's queue, and posts a CE_UPDATE control
            event to the queue.
            
            If AbstractApp.finish() has been called, this will raise
            sbak.error.GameExit, which will cause the control cycle
            to end before any of the above can occur.
        """
        if self._quit:
            raise error.AppExit()
        self.clock.tick(self.ticks)
        self.post_system_events()
        self.post(Event(CONTROLEVENT, control = CE_UPDATE))

    # Other features
    
    # For compatibility...
    get_universe = operator.itemgetter('universe')
    universe = property(
        fget = get_universe,
        doc =
        """ Gives the first child with id "universe".
        
            Currently read-only. (This may change in the future.)
        """
    )
    
    def show_fps(self, val=True, pos=(0,0), bgcolor=(0,0,0),
                 fgcolor=(255,255,255),
                 priority = 1000000):
        """ Call this to create a hook that will draw the game's FPS.
        
            val:    Whether or not to show the FPS. If True (default),
                    the hook is created (unless it already exists). If
                    False, the hook is removed (unless it does not
                    exist.) Defaults to True.
            
            pos:    The position on-screen at which the FPS is drawn.
                    Defaults to 0,0.
            
            bgcolor:The background of the text. Defaults to black.
            
            fgcolor:The color of the text. Defaults to white.
            
            priority:
                    Priority of the draw hook. Defaults to 1000000,
                    which is usually high enough to guaruntee that the
                    FPS is always drawn on top of everything else.
                    
            Since many game developers will want to see the FPS while
            developing their game, this method is here to provide an
            easy way to do so.
            
            Calling this function with the default values will create a
            callback that will be hooked to the CE_DISPLAY control
            event handler of the game's event manager. The default
            values will put the FPS in the top-left corner of the
            screen with white text on a black background with a very
            large priority number so that it will almost always appear
            on top of everything else.
            
            To hide the FPS later, call the function again with False
            as the first and only argument.
        """
        # If this is a call to show the FPS, create and hook the FPS callback.
        if val:
            # If an fps hook already exists, remove it.
            if hasattr(self, '_showfpshook'):
                self._showfpshook.remove()
                del self._showfpshook
            
            # Get needed stuff from draw module.
            from sbak.draw import text as draw_text, fix_color
            
            # Fix the arguments.
            pos = map(int, pos[:2])
            fgcolor = fix_color(fgcolor)
            bgcolor = fix_color(bgcolor)
        
            # Create the display callback.
            def display_fps(e):
                fps = self.clock.get_fps()
                ticks = self.ticks
                perc = fps / ticks
                text = "FPS = %.1f / %.1f = %.1f%%" % (fps, ticks, perc)
                
                region = draw_text(e.dest, pos, text, None, fgcolor, bgcolor)
                self.smudge(region)
        
            # Create the hook.
            self._showfpshook = self.events.hook_weak(
                callback = display_fps,
                type = CONTROLEVENT,
                priority = priority,
                control = CE_DISPLAY
            )
        
        # If this is a call to hide the FPS and an FPS displayer exists,
        # remove it.
        elif hasattr(self, "_showfpshook"):
            self._showfpshook.remove()
            del self._showfpshook